package shared.net.protocol;

import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import shared.net.protocol.ServerHeader.Receive;
import shared.net.protocol.ServerHeader.Send;
import shared.util.PacketIO;
import shared.util.Pair;

/**
 * This class will define and create the various packets needed for
 * communication in the form of a byte array.
 *
 * @author Vincent Tsuei
 * @author Andrew Gratta
 * @author Nathan Poslusny
 * @version 11/21/12
 */
public final class PacketCreator {

    private static final int PACKET_CREATOR_INITIAL_BUFFER_SIZE = 4096;
    /**
     * In order to not keep creating new byte buffers every time we want a
     * packet, we will use a ThreadLocal store. The idea is that any single
     * thread will only create packets one at a time, and so, we will not need
     * to keep allocating new byte buffers, we can reuse old ones.
     */
    private static final ThreadLocal<ByteBuffer> bufferStore = new ThreadLocal<ByteBuffer>() {
        @Override
        protected ByteBuffer initialValue() {
            return ByteBuffer.allocate(PacketCreator.PACKET_CREATOR_INITIAL_BUFFER_SIZE);
        }
    };

    /**
     * To keep this manageable, we'll organize the packets roughly into
     * categories.
     */
    //Packets related to pinging.
    public static class PING {

        public static byte[] getPingPacket(int pingId) {
            ByteBuffer b = StartPacket(Send.PING);
            b.putInt(pingId);
            return cleanup(b);
        }

        public static byte[] getPongPacket(int pingId) {
            ByteBuffer b = StartPacket(Receive.PONG);
            b.putInt(pingId);
            return cleanup(b);
        }
    }

    //Packets related to logging in.
    public static class LOGIN {

        public static byte[] getLoginRequest(String name) {
            ByteBuffer b = StartPacket(Receive.LOGIN_REQUEST);
            PacketIO.putString(b, name);
            return cleanup(b);
        }

        public static byte[] getLoginSuccessResponse() {
            return PacketCreator.SuccessResponse(Send.LOGIN_RESPONSE);
        }

        public static byte[] getLoginFailureResposne(String errMsg) {
            return PacketCreator.FailureResponse(errMsg, Send.LOGIN_RESPONSE);
        }
    }

    // Packets related to lobby functions - gamelist, playerlist, disconnect, observer, joingame
    public static class LOBBY {

        public static byte[] getGameListRequest() {
            ByteBuffer b = StartPacket(Receive.GAME_LIST_REQUEST);
            return cleanup(b);
        }

        public static byte[] getGameListResponse(HashMap<Long, Pair<String, String>> gamePlayerPairs) {
            ByteBuffer b = StartPacket(Send.GAME_LIST_RESPONSE);
            b.put((byte) 1);
            b.putInt(gamePlayerPairs.size());
            for (Map.Entry<Long, Pair<String, String>> entry : gamePlayerPairs.entrySet()) {
                Long key = entry.getKey();
                b.putLong(key);
                PacketIO.putString(b, entry.getValue().getRight());
                PacketIO.putString(b, entry.getValue().getLeft());
            }
            return cleanup(b);
        }

        public static byte[] getGameListFailureResponse(String errMsg) {
            return PacketCreator.FailureResponse(errMsg, Send.GAME_LIST_RESPONSE);
        }

        public static byte[] getPlayerListRequest() {
            ByteBuffer b = StartPacket(Receive.PLAYER_LIST_REQUEST);
            return cleanup(b);
        }

        public static byte[] getPlayerListResponse(String[] playerList) {
            ByteBuffer b = StartPacket(Send.PLAYER_LIST_RESPONSE);
            b.put((byte) 1);
            b.putInt(playerList.length);
            for (String player : playerList) {
                PacketIO.putString(b, player);
            }
            return cleanup(b);
        }

        public static byte[] getPlayerListFailureResponse(String errMsg) {
            return PacketCreator.FailureResponse(errMsg, Send.PLAYER_LIST_RESPONSE);
        }

        public static class OBSERVE {

            public static byte[] getObserveGameRequest(long roomID) {
                ByteBuffer b = StartPacket(Receive.OBSERVE_GAME_REQUEST);
                b.put((byte) 1);
                b.putLong(roomID);
                return cleanup(b);
            }

            public static byte[] getUnobserveSuccessResponse() {
                return PacketCreator.SuccessResponse(Send.OBSERVE_GAME_RESPONSE);
            }
            
            public static byte[] getObserveSuccessResponse(long gameID, String p1, String p2, String turn, int piles, int[] pileNums) {
                ByteBuffer b = StartPacket(Send.OBSERVE_GAME_RESPONSE);
                b.put((byte) 1);
                b.putLong(gameID);
                PacketIO.putString(b, p1);
                PacketIO.putString(b, p2);
                PacketIO.putString(b, turn);
                b.putInt(piles);
                for (int i : pileNums) {
                    b.putInt(i);
                }
                b.put((byte) 0);
                return cleanup(b);
            }

            public static byte[] getObserveFailureResponse(String errMsg) {
                return PacketCreator.FailureResponse(errMsg, Send.OBSERVE_GAME_RESPONSE);
            }
            
            public static byte[] getObserveUpdateNotify(long gameID, String p1, String p2, String turn, int piles, int[] pileNums, String winner) {
                ByteBuffer b = StartPacket(Send.OBSERVE_GAME_UPDATE);
                b.putLong(gameID);
                PacketIO.putString(b, p1);
                PacketIO.putString(b, p2);
                PacketIO.putString(b, turn);
                b.putInt(piles);
                for (int i : pileNums) {
                    b.putInt(i);
                }
                if (winner == null) {
                    b.put((byte) 0);
                } else {
                    b.put((byte) 1);
                    PacketIO.putString(b, p1);
                }
                return cleanup(b);
            }
        }

        public static class PLAY {

            /**
             * Creates a packet for requesting to start a new game with the
             * specified player
             *
             * @param playerName The username of the second player to start a
             * game with
             * @return
             */
            public static byte[] getNewGameRequest(String playerName) {
                ByteBuffer b = StartPacket(Receive.NEW_GAME_REQUEST);
                PacketIO.putString(b, playerName);
                return cleanup(b);
            }

            /**
             * Return the packet that signals a failed 'play' command.
             *
             * @return
             */
            public static byte[] getNewGameFailureResponse(String reason) {
                return PacketCreator.FailureResponse(reason, Send.NEW_GAME_RESPONSE);
            }

            /**
             * Signal that an invite was successful.
             */
            public static byte[] getGameInviteSuccess() {
                ByteBuffer b = StartPacket(Send.NEW_GAME_RESPONSE);
                b.put((byte) 1);
                return cleanup(b);
            }

            /**
             * Signal that an invite was sent from the specified player.
             */
            public static byte[] getGameInviteNotify(String inviteFrom) {
                ByteBuffer b = StartPacket(Send.GAME_INVITE_NOTIFY);
                PacketIO.putString(b, inviteFrom);
                return cleanup(b);
            }

            /**
             * Signal that an invite accept was successful.
             *
             * USED FOR THE INVITEE
             */
            public static byte[] getGameAcceptSuccess(long gameID, String opponent, boolean yourTurn, int piles, int[] pileNums) {
                ByteBuffer b = StartPacket(Send.NEW_GAME_RESPONSE);
                b.put((byte) 2);
                putGameInfo(b, gameID, opponent, yourTurn, piles, pileNums);
                return cleanup(b);
            }

            /**
             * Signal that a game has started. Includes the state of the game.
             *
             * USED FOR THE INVITER
             */
            public static byte[] getGameStartNotify(long gameID, String opponent, boolean yourTurn, int piles, int[] pileNums) {
                ByteBuffer b = StartPacket(Send.GAME_START_NOTIFY);
                putGameInfo(b, gameID, opponent, yourTurn, piles, pileNums);
                return cleanup(b);
            }
        }
    }

    // Packets only allowed in a game - player move, unobserve, forfeit
    public static class IN_GAME {

        public static byte[] getPlayerMoveRequest(int n, int s) {
            ByteBuffer b = StartPacket(Receive.PLAYER_MOVE_REQUEST);
            b.putInt(n);
            b.putInt(s);
            return cleanup(b);
        }
        
        public static byte[] getObserveGameEndNotify(long gameId) {
            ByteBuffer b = StartPacket(Send.OBSERVE_GAME_END_NOTIFY);
            b.putLong(gameId);
            return cleanup(b);
        }

        public static byte[] getUnobserveGameRequest(long roomID) {
            ByteBuffer b = StartPacket(Receive.OBSERVE_GAME_REQUEST);
            b.put((byte) 0);
            b.putLong(roomID);
            return cleanup(b);
        }

        public static byte[] getGameUpdate(String msg, long gameID, boolean yourTurn, int piles, int[] pileNums) {
            ByteBuffer b = StartPacket(Send.GAME_UPDATE);
            b.put((byte) 1);
            PacketIO.putString(b, msg);
            putGameInfo(b, gameID, null, yourTurn, piles, pileNums);
            return cleanup(b);
        }
        
        public static byte[] getObserverGameUpdate(String msg)
        {
            ByteBuffer b = StartPacket(Send.GAME_UPDATE);
            PacketIO.putString(b, msg);
            return cleanup(b);
        }

        public static byte[] getPlayerMoveFailureResponse(String errMsg) {
            return PacketCreator.FailureResponse(errMsg, Send.GAME_UPDATE);
        }
        
        public static byte[] getWinNotify() {
            ByteBuffer b = StartPacket(Send.GAME_END_NOTIFY);
            b.put((byte) 1);
            b.put((byte) 1);
            return cleanup(b);
        }
        public static byte[] getLoseNotify() {
            ByteBuffer b = StartPacket(Send.GAME_END_NOTIFY);
            b.put((byte) 1);
            b.put((byte) 0);
            return cleanup(b);
        }
        
        public static byte[] getDisconnectWinNotify() {
            ByteBuffer b = StartPacket(Send.GAME_END_NOTIFY);
            b.put((byte) 1);
            b.put((byte) 1);
            return cleanup(b);
        }
        public static byte[] getDisconnectLoseNotify() {
            ByteBuffer b = StartPacket(Send.GAME_END_NOTIFY);
            b.put((byte) 1);
            b.put((byte) 1);
            return cleanup(b);
        }
        
        public static byte[] getDisconnectEndNotify() {
            ByteBuffer b = StartPacket(Send.GAME_END_NOTIFY);
            b.put((byte) 1);
            b.put((byte) 2);
            return cleanup(b);
        }
    }

    /**
     * Create a success packet of the specified type
     *
     * @param packetType The type of packet response
     * @return
     */
    private static byte[] SuccessResponse(Send packetType) {
        ByteBuffer b = StartPacket(packetType);
        b.put((byte) 1);
        return cleanup(b);
    }

    /**
     * Create a failure packet of the specified type
     *
     * @param errMsg Custom error message to send
     * @param packetType The type of packet response
     * @return
     */
    private static byte[] FailureResponse(String errMsg, Send packetType) {
        ByteBuffer b = StartPacket(packetType);
        b.put((byte) 0);
        PacketIO.putString(b, errMsg);
        //b.putInt(errMsg.length());
        //b.put(errMsg.getBytes());
        return cleanup(b);
    }

////////////////////////////////////////////////////////////////////////////////
    // Below are utility methods to try and make packet creation as neat
    // and compact as possible.
    /**
     * Should be called at the start of every packet creation. Gets the thread's
     * bytebuffer from the store and attaches the packet header.
     */
    private static ByteBuffer StartPacket(Receive r) {
        ByteBuffer b = bufferStore.get();
        b.put(r.getHeaderAsByte());
        return b;
    }

    private static ByteBuffer StartPacket(Send r) {
        ByteBuffer b = bufferStore.get();
        b.put(r.getHeaderAsByte());
        return b;
    }

    /**
     * Should be called at the end of every packet creation. Handles cleanup and
     * transforming tasks. -> Flips the buffer -> Creates a byte array and reads
     * the buffer into it -> Clears the buffer
     */
    private static byte[] cleanup(ByteBuffer buffer) {
        try {
            buffer.flip();
            byte[] ret = new byte[buffer.limit()];
            buffer.get(ret);
            //System.out.println("New Packet: " + Arrays.toString(ret));
            return ret;
        } finally {
            buffer.clear();
        }
    }

    /**
     * Put game information into the specified packet.
     */
    private static void putGameInfo(ByteBuffer b, long gameID, String opponent, boolean yourTurn, int piles, int[] pileNums) {
        b.putLong(gameID);
        if (opponent != null) {
            PacketIO.putString(b, opponent);
        }
        if (yourTurn) {
            b.put((byte) 1);
        } else {
            b.put((byte) 0);
        }
        b.putInt(piles);
        for (int i : pileNums) {
            b.putInt(i);
        }
    }
}
